Skip to content

fix(account-pool): preserve provider session affinity - #5

Closed
smsunarto wants to merge 2 commits into
scott/pool-request-recoveryfrom
scott/pool-session-affinity
Closed

fix(account-pool): preserve provider session affinity#5
smsunarto wants to merge 2 commits into
scott/pool-request-recoveryfrom
scott/pool-session-affinity

Conversation

@smsunarto

@smsunarto smsunarto commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Superseded by get-bb#3117 in the correct upstream repository. Closing this misplaced PR.

Human comments

What was wrong

Concurrent requests from one conversation could move between accounts as in-flight load changed. The pool also dropped Codex's native session-id and thread-id headers. That disrupted stable request routing and upstream session identity.

What changed

Extract session identity once at the provider boundary and keep each provider/host/session on its selected account while that account remains eligible. Rebind when recovery selects another account. Bound the in-memory cache to 4,096 entries with a 30-minute idle expiry and clear it on hub restart.

Read Claude's JSON session metadata and existing legacy session format. Prefer Codex's native session-id, retain its existing session_id input, and use prompt_cache_key as a separately namespaced fallback. Preserve native headers and request payloads, including encrypted compaction and reasoning items. Requests without a usable identifier retain ordinary account selection.

This layer builds on request recovery. Updated the plugin overview and CLI guide. Stable routing supports cache locality. It does not establish actual upstream cache-hit rates or guarantee encrypted-state portability when failover changes accounts.

How you verified

The committed public HTTP/WebSocket regressions failed before implementation: 12 failures covering same-session routing under load, host isolation, idle expiry, account eligibility changes, concurrent first selection, native Codex headers, and LRU eviction.

  • Full plugin Turbo suite: 182 tests passed across 9 files, including 35 parser boundary cases and provider-isolation/restart coverage.
  • Turbo typecheck, explicit-file formatting, oxlint, and the plugin bundle build passed.
  • Original actual-hub probes now keep Claude and Codex requests on account A while its first stream remains open. Codex session/thread headers and the compaction-shaped body arrive unchanged.
  • Three additional failing regressions verify that older requests cannot erase or overwrite newer healthy bindings, including accounts excluded by a request snapshot or already attempted. Standalone actual-hub probes pass for all three races. Ordinary network failover still rebinds.
  • WebSocket continuation tests preserve encrypted reasoning/compaction items and cache metadata. Refresh deduplication, transient refresh recovery, and 401 failover probes still pass.

Tests use synthetic credentials and upstream responses. No live provider cache-hit rate was measured.

Stack

Review and merge in order:

  1. Serialize credential refreshes
  2. Recover transient OAuth failures
  3. Recover requests before streaming
  4. Preserve session affinity

All PRs target get-bb/bb:main. The source branches remain stacked in the fork. GitHub diffs are currently cumulative because upstream access is read-only, so the parent branches cannot be published there.

AGENT GENERATED

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 9 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="plugins/account-pool/src/hub.ts">

<violation number="1" location="plugins/account-pool/src/hub.ts:636">
P2: When an older in-flight request fails on the currently-bound account and a newer concurrent request then re-confirms that same account (so the binding still points to it), the older request's retry sees `bound.account.id === previousAccountId` and rebinds the shared session to a different account. This can disrupt the newer request's stable routing even though that account just succeeded for it. The rebind condition `bound.account.id === previousAccountId` does not distinguish 'account I failed on and nobody rebinding since' from 'account a newer request just re-confirmed', and the existing tests only cover the case where the newer binding points to a different account. Consider tracking whether the binding was re-confirmed by a newer request before allowing an older retry to rebind away from it.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

selected !== null &&
(bound === undefined ||
bound.account.id === selected.account.id ||
bound.account.id === previousAccountId)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When an older in-flight request fails on the currently-bound account and a newer concurrent request then re-confirms that same account (so the binding still points to it), the older request's retry sees bound.account.id === previousAccountId and rebinds the shared session to a different account. This can disrupt the newer request's stable routing even though that account just succeeded for it. The rebind condition bound.account.id === previousAccountId does not distinguish 'account I failed on and nobody rebinding since' from 'account a newer request just re-confirmed', and the existing tests only cover the case where the newer binding points to a different account. Consider tracking whether the binding was re-confirmed by a newer request before allowing an older retry to rebind away from it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At plugins/account-pool/src/hub.ts, line 636:

<comment>When an older in-flight request fails on the currently-bound account and a newer concurrent request then re-confirms that same account (so the binding still points to it), the older request's retry sees `bound.account.id === previousAccountId` and rebinds the shared session to a different account. This can disrupt the newer request's stable routing even though that account just succeeded for it. The rebind condition `bound.account.id === previousAccountId` does not distinguish 'account I failed on and nobody rebinding since' from 'account a newer request just re-confirmed', and the existing tests only cover the case where the newer binding points to a different account. Consider tracking whether the binding was re-confirmed by a newer request before allowing an older retry to rebind away from it.</comment>

<file context>
@@ -598,7 +618,34 @@ export class AccountPoolHub {
+      selected !== null &&
+      (bound === undefined ||
+        bound.account.id === selected.account.id ||
+        bound.account.id === previousAccountId)
+    ) {
+      this.affinityBindings.delete(affinityKey);
</file context>

@smsunarto smsunarto closed this Sep 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant